@@ -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,42 @@ 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+ < button
173+ className = { `${ styles . seasonButton } ${ selectedSeason === 'all' ? styles . activeSeason : '' } ` }
174+ onClick = { ( ) => onSeasonChange ( 'all' ) }
175+ >
176+ All Seasons
177+ </ button >
178+ { availableSeasons
179+ . slice ( )
180+ . reverse ( )
181+ . map ( ( season ) => (
182+ < button
183+ key = { season }
184+ className = { `${ styles . seasonButton } ${ selectedSeason === season ? styles . activeSeason : '' } ` }
185+ onClick = { ( ) => onSeasonChange ( season ) }
186+ >
187+ { season }
188+ </ button >
189+ ) ) }
190+ </ div >
191+ </ div >
192+ )
193+ }
194+ )
195+
196+ SeasonFilter . displayName = 'SeasonFilter'
197+
119198// MatchSection with placeholder heights
120199const MatchSection = React . memo (
121- ( { matchKey, formattedMatches , onTileClick } ) => {
122- const singlesMatches = formattedMatches . filter (
200+ ( { matchKey, seasonFilteredMatches , onTileClick } ) => {
201+ const singlesMatches = seasonFilteredMatches . filter (
123202 ( match ) =>
124203 match . singles &&
125204 ( ( match . matchDetails . duel &&
@@ -129,7 +208,7 @@ const MatchSection = React.memo(
129208 matchKey === `_#${ match . matchDetails . event } ` ) )
130209 )
131210
132- const doublesMatches = formattedMatches . filter (
211+ const doublesMatches = seasonFilteredMatches . filter (
133212 ( match ) =>
134213 ! match . singles &&
135214 ( ( match . matchDetails . duel &&
@@ -142,7 +221,7 @@ const MatchSection = React.memo(
142221 const [ matchName ] = matchKey . split ( '#' )
143222 const displayName =
144223 matchName === '_'
145- ? formattedMatches . find (
224+ ? seasonFilteredMatches . find (
146225 ( match ) =>
147226 ! match . matchDetails . duel && match . matchDetails . event === matchName
148227 ) ?. matchDetails . event || matchName
@@ -231,6 +310,7 @@ const Dashboard = () => {
231310 const [ selectedMatchSets , setSelectedMatchSets ] = useState ( [ ] )
232311 const [ isMobile , setIsMobile ] = useState ( false )
233312 const [ isLoaded , setIsLoaded ] = useState ( false )
313+ const [ selectedSeason , setSelectedSeason ] = useState ( getCurrentSeason ( ) )
234314
235315 const [ searchIndex , setSearchIndex ] = useState ( null )
236316 // Calculate formatted matches once
@@ -243,11 +323,33 @@ const Dashboard = () => {
243323 return formatted
244324 } , [ matches , isLoaded ] )
245325
246- const uniqueMatches = useMemo (
247- ( ) => getUniqueMatches ( formattedMatches , cleanTeamName ) ,
248- [ formattedMatches ]
326+ // Filter matches by selected season
327+ const seasonFilteredMatches = useMemo ( ( ) => {
328+ if ( selectedSeason === 'all' ) {
329+ return formattedMatches
330+ }
331+ return formattedMatches . filter (
332+ ( match ) => getMatchSeason ( match . matchDate ) === selectedSeason
333+ )
334+ } , [ formattedMatches , selectedSeason ] )
335+
336+ const handleSeasonChange = useCallback (
337+ ( season ) => {
338+ setSelectedSeason ( season )
339+ setSelectedMatchSets ( [ ] ) // Clear carousel selections when season changes
340+ } ,
341+ [ setSelectedSeason , setSelectedMatchSets ]
249342 )
250343
344+ // Get available seasons from all matches
345+ const availableSeasons = useMemo ( ( ) => {
346+ return getAvailableSeasons ( formattedMatches )
347+ } , [ formattedMatches ] )
348+
349+ const uniqueMatches = useMemo ( ( ) => {
350+ return getUniqueMatches ( seasonFilteredMatches , cleanTeamName )
351+ } , [ seasonFilteredMatches ] )
352+
251353 // Mobile detection useEffect
252354 useEffect ( ( ) => {
253355 const checkIfMobile = ( ) => setIsMobile ( window . innerWidth < 400 )
@@ -259,11 +361,11 @@ const Dashboard = () => {
259361 return ( ) => resizeObserver . disconnect ( )
260362 } , [ ] )
261363
262- // Create search index after component mounts and formattedMatches is available
364+ // Create search index after component mounts and seasonFilteredMatches is available
263365 useEffect ( ( ) => {
264- if ( formattedMatches . length && ! searchIndex ) {
366+ if ( seasonFilteredMatches . length && ! searchIndex ) {
265367 const timer = setTimeout ( ( ) => {
266- const fuse = new Fuse ( formattedMatches , {
368+ const fuse = new Fuse ( seasonFilteredMatches , {
267369 keys : searchableProperties ,
268370 threshold : 0.4
269371 } )
@@ -272,7 +374,17 @@ const Dashboard = () => {
272374
273375 return ( ) => clearTimeout ( timer )
274376 }
275- } , [ formattedMatches , searchIndex ] ) // Now formattedMatches is defined before this useEffect
377+ } , [ seasonFilteredMatches , searchIndex , searchableProperties ] )
378+
379+ useEffect ( ( ) => {
380+ if ( seasonFilteredMatches . length ) {
381+ const fuse = new Fuse ( seasonFilteredMatches , {
382+ keys : searchableProperties ,
383+ threshold : 0.4
384+ } )
385+ setSearchIndex ( fuse )
386+ }
387+ } , [ selectedSeason , searchableProperties ] )
276388
277389 // Filtered match sets calculation using the searchIndex from state
278390 const filteredMatchSets = useMemo ( ( ) => {
@@ -304,14 +416,14 @@ const Dashboard = () => {
304416 if ( selectedMatchSets . length > 0 ) return selectedMatchSets
305417 return [
306418 ...new Set (
307- formattedMatches . map ( ( match ) => {
419+ seasonFilteredMatches . map ( ( match ) => {
308420 return match . matchDetails . duel
309421 ? `${ match . matchDate } #${ match . teams . opponentTeam } `
310422 : `_#${ match . matchDetails . event } `
311423 } )
312424 )
313425 ]
314- } , [ searchTerm , filteredMatchSets , selectedMatchSets , formattedMatches ] )
426+ } , [ searchTerm , filteredMatchSets , selectedMatchSets ] )
315427
316428 const handleTileClick = useCallback (
317429 ( videoId ) => {
@@ -347,8 +459,14 @@ const Dashboard = () => {
347459 </ div >
348460 </ header >
349461
462+ < SeasonFilter
463+ availableSeasons = { availableSeasons }
464+ selectedSeason = { selectedSeason }
465+ onSeasonChange = { handleSeasonChange }
466+ />
467+
350468 < div className = { styles . carousel } >
351- { ! formattedMatches . length
469+ { ! seasonFilteredMatches . length
352470 ? Array ( 10 )
353471 . fill ( 0 )
354472 . map ( ( _ , i ) => (
@@ -386,7 +504,7 @@ const Dashboard = () => {
386504 < div className = { styles . noMatches } > No matches found</ div >
387505 ) : (
388506 displayMatchSets . map ( ( matchKey , index ) => {
389- const singlesMatches = formattedMatches . filter (
507+ const singlesMatches = seasonFilteredMatches . filter (
390508 ( match ) =>
391509 match . singles &&
392510 ( ( match . matchDetails . duel &&
@@ -396,7 +514,7 @@ const Dashboard = () => {
396514 matchKey === `_#${ match . matchDetails . event } ` ) )
397515 )
398516
399- const doublesMatches = formattedMatches . filter (
517+ const doublesMatches = seasonFilteredMatches . filter (
400518 ( match ) =>
401519 ! match . singles &&
402520 ( ( match . matchDetails . duel &&
0 commit comments