diff --git a/src/pages/Viewer.jsx b/src/pages/Viewer.jsx index fe1a992..cd0a244 100644 --- a/src/pages/Viewer.jsx +++ b/src/pages/Viewer.jsx @@ -18,7 +18,8 @@ import { getRankingsForEvent, getSkillsForEvent, getEventsForTeam, - getActiveSeasons + getActiveSeasons, + searchEvents } from '../services/robotevents'; import { extractVideoId, getStreamStartTime } from '../services/youtube'; import { findWebcastCandidates } from '../services/webcastDetection'; @@ -118,6 +119,11 @@ function Viewer() { const [eventUrl, setEventUrl] = useState(''); const [teamNumber, setTeamNumber] = useState(''); + // Search State + const [searchResults, setSearchResults] = useState([]); + const [isSearching, setIsSearching] = useState(false); + const [showSearchResults, setShowSearchResults] = useState(false); + // UI State /** @type {'search' | 'list' | 'matches'} */ const [activeTab, setActiveTab] = useState('list'); @@ -598,12 +604,7 @@ function Viewer() { } }, [streams, activeStreamId]); - const handleEventSearch = async () => { - if (!eventUrl.trim()) { - setError('Please enter an event URL'); - return; - } - + const loadEvent = async (sku) => { isInternalLoading.current = true; setEventLoading(true); setError(''); @@ -612,11 +613,6 @@ function Viewer() { urlPresetRef.current = null; // Sync ref immediately try { - const skuMatch = eventUrl.match(/(RE-[A-Z0-9]+-\d{2}-\d{4})/); - if (!skuMatch) { - throw new Error('Invalid RobotEvents URL. Could not find SKU.'); - } - const sku = skuMatch[1]; setUrlSku(sku); // Set SKU immediately const foundEvent = await getEventBySku(sku); @@ -661,8 +657,6 @@ function Viewer() { idx === 0 ? { ...s, url: cached.url, videoId: cached.videoId } : s )); } - - } catch (err) { setError(err.message); } finally { @@ -672,6 +666,59 @@ function Viewer() { } }; + const performSearch = async (query) => { + if (!query.trim()) { + setSearchResults([]); + setShowSearchResults(false); + return; + } + + setIsSearching(true); + try { + const results = await searchEvents(query); + setSearchResults(results); + setShowSearchResults(true); // Always show, empty list handling in UI if needed + } catch (err) { + console.error(err); + } finally { + setIsSearching(false); + } + }; + + // Debounced Search Effect + useEffect(() => { + // Don't auto-search if deep linking or if it looks like a SKU (let user finish typing or hit enter for SKU) + if (isDeepLinking || !eventUrl.trim()) return; + + const timer = setTimeout(() => { + // Check if it matches SKU pattern to avoid searching for partial SKUs unnecessarily? + // Actually, we WANT to search names. SKU detection happens on 'Enter' or click. + // If it IS a SKU, performSearch will return it as a result anyway (searchEvents handles SKU). + performSearch(eventUrl); + }, 500); + + return () => clearTimeout(timer); + }, [eventUrl, isDeepLinking]); + + const handleEventSearch = async () => { + if (!eventUrl.trim()) { + setError('Please enter an event URL or search term'); + return; + } + + setShowSearchResults(false); + + // Check for SKU pattern match first for direct load + const skuMatch = eventUrl.match(/(RE-[A-Z0-9]+-\d{2}-\d{4})/); + if (skuMatch) { + await loadEvent(skuMatch[1]); + return; + } + + // Force immediate search if not SKU + performSearch(eventUrl); + }; + const handleWebcastSelect = (selectedVideoId, selectedUrl, method) => { // Populate first stream with selected webcast setStreams(prev => prev.map((s, idx) => @@ -1593,7 +1640,7 @@ function Viewer() { {/* Right Column: Controls */}