@@ -39,6 +39,53 @@ const formatMatches = (matches) => {
3939 . sort ( ( a , b ) => new Date ( b . matchDate ) - new Date ( a . matchDate ) )
4040}
4141
42+ // Helper function to determine which season a match belongs to
43+ const getMatchSeason = ( matchDate ) => {
44+ const date = new Date ( matchDate )
45+ const month = date . getMonth ( ) + 1 // 1-12
46+ const year = date . getFullYear ( )
47+
48+ // College tennis seasons:
49+ // Fall (Aug-Nov): part of next year's season (e.g., Aug 2024 = 2024-2025)
50+ // Spring (Jan-May): part of current year's season (e.g., Jan 2025 = 2024-2025)
51+
52+ if ( month >= 8 ) {
53+ // Aug-Dec: part of current-next season
54+ return `${ year } -${ year + 1 } `
55+ } else if ( month <= 5 ) {
56+ // Jan-May: part of previous-current season
57+ return `${ year - 1 } -${ year } `
58+ }
59+ // June-July: off-season, still count as previous-current season
60+ return `${ year - 1 } -${ year } `
61+ }
62+
63+ // Get current season based on today's date
64+ const getCurrentSeason = ( ) => {
65+ return getMatchSeason ( new Date ( ) . toISOString ( ) . split ( 'T' ) [ 0 ] )
66+ }
67+
68+ // Get all unique seasons from matches
69+ const getAvailableSeasons = ( matches ) => {
70+ const seasons = new Set ( )
71+ const currentYear = new Date ( ) . getFullYear ( )
72+
73+ matches . forEach ( ( match ) => {
74+ const season = getMatchSeason ( match . matchDate )
75+
76+ // Only include seasons from the last 10 years
77+ const seasonStartYear = parseInt ( season . split ( '-' ) [ 0 ] )
78+ if (
79+ seasonStartYear >= currentYear - 30 &&
80+ seasonStartYear <= currentYear + 1
81+ ) {
82+ seasons . add ( season )
83+ }
84+ } )
85+
86+ return Array . from ( seasons ) . sort ( ) . reverse ( ) // Most recent first
87+ }
88+
4289// Memoized CarouselItem with fixed dimensions but original logo container
4390const CarouselItem = React . memo ( ( { match, isSelected, onClick, logo } ) => {
4491 const matchKey = match . matchDetails . duel
@@ -116,10 +163,39 @@ const SearchBox = React.memo(({ searchTerm, onSearch, onClear }) => {
116163
117164SearchBox . displayName = 'SearchBox'
118165
166+ // Season Filter Component
167+ const SeasonFilter = React . memo (
168+ ( { availableSeasons, selectedSeason, onSeasonChange } ) => {
169+ return (
170+ < div className = { styles . seasonFilterContainer } >
171+ < div className = { styles . seasonButtons } >
172+ { availableSeasons . map ( ( season ) => (
173+ < button
174+ key = { season }
175+ className = { `${ styles . seasonButton } ${ selectedSeason === season ? styles . activeSeason : '' } ` }
176+ onClick = { ( ) => onSeasonChange ( season ) }
177+ >
178+ { season }
179+ </ button >
180+ ) ) }
181+ < button
182+ className = { `${ styles . seasonButton } ${ selectedSeason === 'all' ? styles . activeSeason : '' } ` }
183+ onClick = { ( ) => onSeasonChange ( 'all' ) }
184+ >
185+ All Seasons
186+ </ button >
187+ </ div >
188+ </ div >
189+ )
190+ }
191+ )
192+
193+ SeasonFilter . displayName = 'SeasonFilter'
194+
119195// MatchSection with placeholder heights
120196const MatchSection = React . memo (
121- ( { matchKey, formattedMatches , onTileClick } ) => {
122- const singlesMatches = formattedMatches . filter (
197+ ( { matchKey, seasonFilteredMatches , onTileClick } ) => {
198+ const singlesMatches = seasonFilteredMatches . filter (
123199 ( match ) =>
124200 match . singles &&
125201 ( ( match . matchDetails . duel &&
@@ -129,7 +205,7 @@ const MatchSection = React.memo(
129205 matchKey === `_#${ match . matchDetails . event } ` ) )
130206 )
131207
132- const doublesMatches = formattedMatches . filter (
208+ const doublesMatches = seasonFilteredMatches . filter (
133209 ( match ) =>
134210 ! match . singles &&
135211 ( ( match . matchDetails . duel &&
@@ -142,7 +218,7 @@ const MatchSection = React.memo(
142218 const [ matchName ] = matchKey . split ( '#' )
143219 const displayName =
144220 matchName === '_'
145- ? formattedMatches . find (
221+ ? seasonFilteredMatches . find (
146222 ( match ) =>
147223 ! match . matchDetails . duel && match . matchDetails . event === matchName
148224 ) ?. matchDetails . event || matchName
@@ -231,6 +307,7 @@ const Dashboard = () => {
231307 const [ selectedMatchSets , setSelectedMatchSets ] = useState ( [ ] )
232308 const [ isMobile , setIsMobile ] = useState ( false )
233309 const [ isLoaded , setIsLoaded ] = useState ( false )
310+ const [ selectedSeason , setSelectedSeason ] = useState ( getCurrentSeason ( ) )
234311
235312 const [ searchIndex , setSearchIndex ] = useState ( null )
236313 // Calculate formatted matches once
@@ -243,11 +320,33 @@ const Dashboard = () => {
243320 return formatted
244321 } , [ matches , isLoaded ] )
245322
246- const uniqueMatches = useMemo (
247- ( ) => getUniqueMatches ( formattedMatches , cleanTeamName ) ,
248- [ formattedMatches ]
323+ // Filter matches by selected season
324+ const seasonFilteredMatches = useMemo ( ( ) => {
325+ if ( selectedSeason === 'all' ) {
326+ return formattedMatches
327+ }
328+ return formattedMatches . filter (
329+ ( match ) => getMatchSeason ( match . matchDate ) === selectedSeason
330+ )
331+ } , [ formattedMatches , selectedSeason ] )
332+
333+ const handleSeasonChange = useCallback (
334+ ( season ) => {
335+ setSelectedSeason ( season )
336+ setSelectedMatchSets ( [ ] ) // Clear carousel selections when season changes
337+ } ,
338+ [ setSelectedSeason , setSelectedMatchSets ]
249339 )
250340
341+ // Get available seasons from all matches
342+ const availableSeasons = useMemo ( ( ) => {
343+ return getAvailableSeasons ( formattedMatches )
344+ } , [ formattedMatches ] )
345+
346+ const uniqueMatches = useMemo ( ( ) => {
347+ return getUniqueMatches ( seasonFilteredMatches , cleanTeamName )
348+ } , [ ] )
349+
251350 // Mobile detection useEffect
252351 useEffect ( ( ) => {
253352 const checkIfMobile = ( ) => setIsMobile ( window . innerWidth < 400 )
@@ -259,11 +358,11 @@ const Dashboard = () => {
259358 return ( ) => resizeObserver . disconnect ( )
260359 } , [ ] )
261360
262- // Create search index after component mounts and formattedMatches is available
361+ // Create search index after component mounts and seasonFilteredMatches is available
263362 useEffect ( ( ) => {
264- if ( formattedMatches . length && ! searchIndex ) {
363+ if ( seasonFilteredMatches . length && ! searchIndex ) {
265364 const timer = setTimeout ( ( ) => {
266- const fuse = new Fuse ( formattedMatches , {
365+ const fuse = new Fuse ( seasonFilteredMatches , {
267366 keys : searchableProperties ,
268367 threshold : 0.4
269368 } )
@@ -272,7 +371,17 @@ const Dashboard = () => {
272371
273372 return ( ) => clearTimeout ( timer )
274373 }
275- } , [ formattedMatches , searchIndex ] ) // Now formattedMatches is defined before this useEffect
374+ } , [ seasonFilteredMatches , searchIndex , searchableProperties ] )
375+
376+ useEffect ( ( ) => {
377+ if ( seasonFilteredMatches . length ) {
378+ const fuse = new Fuse ( seasonFilteredMatches , {
379+ keys : searchableProperties ,
380+ threshold : 0.4
381+ } )
382+ setSearchIndex ( fuse )
383+ }
384+ } , [ selectedSeason , searchableProperties ] )
276385
277386 // Filtered match sets calculation using the searchIndex from state
278387 const filteredMatchSets = useMemo ( ( ) => {
@@ -304,14 +413,14 @@ const Dashboard = () => {
304413 if ( selectedMatchSets . length > 0 ) return selectedMatchSets
305414 return [
306415 ...new Set (
307- formattedMatches . map ( ( match ) => {
416+ seasonFilteredMatches . map ( ( match ) => {
308417 return match . matchDetails . duel
309418 ? `${ match . matchDate } #${ match . teams . opponentTeam } `
310419 : `_#${ match . matchDetails . event } `
311420 } )
312421 )
313422 ]
314- } , [ searchTerm , filteredMatchSets , selectedMatchSets , formattedMatches ] )
423+ } , [ searchTerm , filteredMatchSets , selectedMatchSets ] )
315424
316425 const handleTileClick = useCallback (
317426 ( videoId ) => {
@@ -339,16 +448,23 @@ const Dashboard = () => {
339448 < header className = { styles . header } >
340449 < div className = { styles . headerContent } >
341450 < h2 > Dashboard</ h2 >
342- < SearchBox
343- searchTerm = { searchTerm }
344- onSearch = { handleSearch }
345- onClear = { handleClearSearch }
346- />
451+ < div className = { styles . headerControls } >
452+ < SearchBox
453+ searchTerm = { searchTerm }
454+ onSearch = { handleSearch }
455+ onClear = { handleClearSearch }
456+ />
457+ < SeasonFilter
458+ availableSeasons = { availableSeasons }
459+ selectedSeason = { selectedSeason }
460+ onSeasonChange = { handleSeasonChange }
461+ />
462+ </ div >
347463 </ div >
348464 </ header >
349465
350466 < div className = { styles . carousel } >
351- { ! formattedMatches . length
467+ { ! seasonFilteredMatches . length
352468 ? Array ( 10 )
353469 . fill ( 0 )
354470 . map ( ( _ , i ) => (
@@ -386,7 +502,7 @@ const Dashboard = () => {
386502 < div className = { styles . noMatches } > No matches found</ div >
387503 ) : (
388504 displayMatchSets . map ( ( matchKey , index ) => {
389- const singlesMatches = formattedMatches . filter (
505+ const singlesMatches = seasonFilteredMatches . filter (
390506 ( match ) =>
391507 match . singles &&
392508 ( ( match . matchDetails . duel &&
@@ -396,7 +512,7 @@ const Dashboard = () => {
396512 matchKey === `_#${ match . matchDetails . event } ` ) )
397513 )
398514
399- const doublesMatches = formattedMatches . filter (
515+ const doublesMatches = seasonFilteredMatches . filter (
400516 ( match ) =>
401517 ! match . singles &&
402518 ( ( match . matchDetails . duel &&
0 commit comments