From 30957c75a57efbd46f0202d9790b9074f9ab2b64 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 19:15:27 +0000 Subject: [PATCH 1/3] feat: Add event searching and responsive footer - Implemented `searchEvents` service to search for events by name or SKU. - Added search functionality to the "Robotevents.com link" section. - Added a dropdown to display search results. - Users can now search for events by name or SKU and load them directly. - Updated footer to hide copyright text on mobile devices (`hidden sm:block`). Co-authored-by: axcdeng <121896294+axcdeng@users.noreply.github.com> --- src/pages/Viewer.jsx | 95 +++++++++++++++++++++++++++++-------- src/services/robotevents.js | 25 ++++++++++ 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/src/pages/Viewer.jsx b/src/pages/Viewer.jsx index fe1a992..da5e84f 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,39 @@ function Viewer() { } }; + const handleEventSearch = async () => { + if (!eventUrl.trim()) { + setError('Please enter an event URL or search term'); + return; + } + + setShowSearchResults(false); + + // Check for SKU pattern match first + const skuMatch = eventUrl.match(/(RE-[A-Z0-9]+-\d{2}-\d{4})/); + if (skuMatch) { + await loadEvent(skuMatch[1]); + return; + } + + // Otherwise perform search + setIsSearching(true); + setError(''); + try { + const results = await searchEvents(eventUrl); + setSearchResults(results); + if (results.length === 0) { + setError('No events found.'); + } else { + setShowSearchResults(true); + } + } catch (err) { + setError(err.message); + } finally { + setIsSearching(false); + } + }; + const handleWebcastSelect = (selectedVideoId, selectedUrl, method) => { // Populate first stream with selected webcast setStreams(prev => prev.map((s, idx) => @@ -1645,9 +1672,9 @@ function Viewer() {
-
+
e.key === 'Enter' && handleEventSearch()} /> + + {/* Search Results Dropdown */} + {showSearchResults && searchResults.length > 0 && ( +
+ {searchResults.map((evt) => ( + + ))} +
+ )}
@@ -2508,7 +2563,7 @@ function Viewer() { /> {/* Copyright Footer */} -