11import { useEffect , useRef , useState , useCallback } from 'react' ;
22import { useSelector } from 'react-redux' ;
33import axios from 'axios' ;
4- import DatePicker , { CalendarContainer } from 'react-datepicker' ;
4+ import DatePicker from 'react-datepicker' ;
55import 'react-datepicker/dist/react-datepicker.css' ;
66import * as d3 from 'd3' ;
7+ import { FaTrash } from 'react-icons/fa' ;
78import styles from './MostFrequentKeywords.module.css' ;
89import Select , { components as selectComponents } from 'react-select' ;
9- import PropTypes from 'prop-types' ;
10-
1110const formatCalendarMonth = date =>
1211 date . toLocaleString ( 'en-US' , {
1312 month : 'long' ,
@@ -20,7 +19,53 @@ const DropdownIndicator = props => (
2019 </ selectComponents . DropdownIndicator >
2120) ;
2221
23- function MostFrequentKeywords ( { darkMode : propDarkMode } ) {
22+ // Pick the most recent unique-tag items, capped at maxItems.
23+ function getLatestData ( data , isMobile ) {
24+ if ( ! data || data . length === 0 ) return [ ] ;
25+
26+ const sorted = [ ...data ] . sort ( ( a , b ) => new Date ( b . date ) - new Date ( a . date ) ) ;
27+ const maxItems = isMobile ? 6 : 8 ;
28+
29+ if ( sorted . length < maxItems ) return sorted ;
30+
31+ const latestItems = [ ] ;
32+ const usedTags = new Set ( ) ;
33+
34+ for ( const item of sorted ) {
35+ if ( ! usedTags . has ( item . tag ) ) {
36+ latestItems . push ( item ) ;
37+ usedTags . add ( item . tag ) ;
38+ if ( latestItems . length >= maxItems ) break ;
39+ }
40+ }
41+
42+ return latestItems ;
43+ }
44+
45+ // Items without a real date are always included; otherwise check the bounds.
46+ function isWithinDateRange ( item , startDate , endDate ) {
47+ if ( ! item . date ) return true ;
48+
49+ const itemDate = new Date ( item . date ) ;
50+ itemDate . setHours ( 0 , 0 , 0 , 0 ) ;
51+
52+ if ( startDate ) {
53+ const start = new Date ( startDate ) ;
54+ start . setHours ( 0 , 0 , 0 , 0 ) ;
55+ if ( itemDate < start ) return false ;
56+ }
57+
58+ if ( endDate ) {
59+ const end = new Date ( endDate ) ;
60+ end . setHours ( 23 , 59 , 59 , 999 ) ;
61+ if ( itemDate > end ) return false ;
62+ }
63+
64+ return true ;
65+ }
66+
67+ function MostFrequentKeywords ( ) {
68+ const darkMode = useSelector ( state => state . theme . darkMode ) ;
2469 const svgRef = useRef ( ) ;
2570 const containerRef = useRef ( ) ;
2671 const [ projects , setProjects ] = useState ( [ ] ) ;
@@ -35,8 +80,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
3580 const [ isMobile , setIsMobile ] = useState ( false ) ;
3681 const [ tooltip , setTooltip ] = useState ( { visible : false , text : '' , x : 0 , y : 0 } ) ;
3782 const API_BASE = process . env . REACT_APP_APIENDPOINT ;
38- const reduxDarkMode = useSelector ( state => state . theme . darkMode ) ;
39- const darkMode = propDarkMode !== undefined ? propDarkMode : reduxDarkMode ;
4083 const palette = darkMode
4184 ? {
4285 controlBg : '#243447' ,
@@ -145,36 +188,7 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
145188 }
146189 } ;
147190
148- // Generate clean data for any project
149- const generateProjectSpecificData = projectName => {
150- const isDuplicableCityCenter = projectName . toLowerCase ( ) . includes ( 'duplicable city center' ) ;
151-
152- if ( isDuplicableCityCenter ) {
153- return [
154- { tag : 'Modular Design' , count : 85 , date : '2025-03-15' } ,
155- { tag : 'Prefabrication' , count : 78 , date : '2025-04-22' } ,
156- { tag : 'Replicable Units' , count : 72 , date : '2025-05-10' } ,
157- { tag : 'Standard Parts' , count : 64 , date : '2025-06-18' } ,
158- { tag : 'Urban Planning' , count : 81 , date : '2025-08-30' } ,
159- { tag : 'Smart City Tech' , count : 69 , date : '2025-10-05' } ,
160- { tag : 'Energy Efficiency' , count : 76 , date : '2026-01-19' } ,
161- { tag : 'Mixed Use' , count : 68 , date : '2026-05-08' } ,
162- ] ;
163- }
164-
165- return [
166- { tag : 'Site Planning' , count : 72 , date : '2024-03-15' } ,
167- { tag : 'Foundation' , count : 65 , date : '2024-06-22' } ,
168- { tag : 'Framing' , count : 58 , date : '2024-09-10' } ,
169- { tag : 'Electrical' , count : 62 , date : '2025-01-18' } ,
170- { tag : 'Plumbing' , count : 54 , date : '2025-04-25' } ,
171- { tag : 'HVAC' , count : 67 , date : '2025-07-30' } ,
172- { tag : 'Finishing' , count : 59 , date : '2025-11-14' } ,
173- { tag : 'Landscaping' , count : 51 , date : '2026-02-05' } ,
174- ] ;
175- } ;
176-
177- const fetchProjectData = async ( projectId , projectName ) => {
191+ const fetchProjectData = async projectId => {
178192 try {
179193 setIsLoading ( true ) ;
180194 setError ( '' ) ;
@@ -192,29 +206,21 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
192206
193207 const responseData = response ?. data ?. data ;
194208 if ( responseData && responseData . length > 0 ) {
195- const dataWithDates = responseData . slice ( 0 , 8 ) . map ( ( item , index ) => {
196- const years = [ 2023 , 2024 , 2025 , 2026 ] ;
197- const year = years [ index % 4 ] ;
198- const month = ( ( index * 3 ) % 12 ) + 1 ;
199- const day = ( ( index * 5 ) % 28 ) + 1 ;
200- return {
201- ...item ,
202- count : item . count || 50 + index * 5 ,
203- date : `${ year } -${ month . toString ( ) . padStart ( 2 , '0' ) } -${ day
204- . toString ( )
205- . padStart ( 2 , '0' ) } `,
206- } ;
207- } ) ;
208- setAllTags ( dataWithDates ) ;
209+ const normalizedData = responseData . slice ( 0 , 8 ) . map ( item => ( {
210+ tag : item . tag ,
211+ count : item . count || 1 ,
212+ date : item . date || null ,
213+ } ) ) ;
214+ setAllTags ( normalizedData ) ;
209215 return ;
210216 }
217+
218+ // API returned empty — no keywords for this project
219+ setAllTags ( [ ] ) ;
211220 } catch {
212- // Use generated data when API fails
221+ setError ( 'Failed to load keywords. Please try again.' ) ;
222+ setAllTags ( [ ] ) ;
213223 }
214-
215- // Fallback to generated data
216- const generatedData = generateProjectSpecificData ( projectName ) ;
217- setAllTags ( generatedData ) ;
218224 } finally {
219225 setIsLoading ( false ) ;
220226 }
@@ -234,7 +240,7 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
234240 } else if ( selected . type === 'project' ) {
235241 const project = projects . find ( p => p . _id === selected . value ) ;
236242 if ( project ) {
237- fetchProjectData ( project . _id , project . projectName ) ;
243+ fetchProjectData ( project . _id ) ;
238244 }
239245 }
240246 } ;
@@ -280,79 +286,25 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
280286 } ;
281287 } , [ dimensions , isMobile ] ) ;
282288
283- const getLatestData = useCallback (
284- data => {
285- if ( ! data || data . length === 0 ) return [ ] ;
286-
287- const sorted = [ ...data ] . sort ( ( a , b ) => new Date ( b . date ) - new Date ( a . date ) ) ;
288- const maxItems = isMobile ? 6 : 8 ;
289-
290- if ( sorted . length >= maxItems ) {
291- const latestItems = [ ] ;
292- const usedTags = new Set ( ) ;
293-
294- for ( const item of sorted ) {
295- if ( ! usedTags . has ( item . tag ) ) {
296- latestItems . push ( item ) ;
297- usedTags . add ( item . tag ) ;
298- if ( latestItems . length >= maxItems ) break ;
299- }
300- }
301-
302- return latestItems ;
303- }
304-
305- return sorted ;
306- } ,
307- [ isMobile ] ,
308- ) ;
309-
310289 const filterTagsByDate = useCallback (
311290 tagsToFilter => {
312291 if ( ! tagsToFilter || tagsToFilter . length === 0 ) return [ ] ;
313292
314293 if ( ! startDate && ! endDate ) {
315- return getLatestData ( tagsToFilter ) ;
294+ return getLatestData ( tagsToFilter , isMobile ) ;
316295 }
317296
318- const filtered = tagsToFilter . filter ( item => {
319- const itemDate = new Date ( item . date ) ;
320- itemDate . setHours ( 0 , 0 , 0 , 0 ) ;
321-
322- if ( startDate && endDate ) {
323- const start = new Date ( startDate ) ;
324- start . setHours ( 0 , 0 , 0 , 0 ) ;
325- const end = new Date ( endDate ) ;
326- end . setHours ( 23 , 59 , 59 , 999 ) ;
327- return itemDate >= start && itemDate <= end ;
328- }
329- if ( startDate ) {
330- const start = new Date ( startDate ) ;
331- start . setHours ( 0 , 0 , 0 , 0 ) ;
332- return itemDate >= start ;
333- }
334- if ( endDate ) {
335- const end = new Date ( endDate ) ;
336- end . setHours ( 23 , 59 , 59 , 999 ) ;
337- return itemDate <= end ;
338- }
339-
340- return true ;
341- } ) ;
297+ const filtered = tagsToFilter . filter ( item => isWithinDateRange ( item , startDate , endDate ) ) ;
342298
343299 const sorted = [ ...filtered ] . sort ( ( a , b ) => b . count - a . count ) ;
344300 const maxItems = isMobile ? 6 : 8 ;
345301 const result = sorted . slice ( 0 , maxItems ) ;
346302
347- if ( result . length === 0 ) {
348- setError ( 'No data for selected range' ) ;
349- } else {
350- setError ( '' ) ;
351- }
303+ setError ( result . length === 0 ? 'No keywords available for selected filters' : '' ) ;
352304
353305 return result ;
354306 } ,
355- [ startDate , endDate , getLatestData , isMobile ] ,
307+ [ startDate , endDate , isMobile ] ,
356308 ) ;
357309
358310 useEffect ( ( ) => {
@@ -1055,58 +1007,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
10551007 backgroundColor : palette . menuBg ,
10561008 } ) ;
10571009
1058- const applyDarkCalendarTheme = useCallback ( ( ) => {
1059- requestAnimationFrame ( ( ) => {
1060- const poppers = Array . from ( document . querySelectorAll ( '.react-datepicker-popper' ) ) ;
1061- const activePopper = poppers . find ( popper => popper . offsetParent !== null ) || poppers . at ( - 1 ) ;
1062- if ( ! activePopper ) return ;
1063-
1064- const datepicker = activePopper . querySelector ( '.react-datepicker' ) ;
1065- const monthContainer = activePopper . querySelector ( '.react-datepicker__month-container' ) ;
1066- const header = activePopper . querySelector ( '.react-datepicker__header' ) ;
1067- const currentMonth = activePopper . querySelector ( '.react-datepicker__current-month' ) ;
1068- const dayNames = activePopper . querySelectorAll ( '.react-datepicker__day-name' ) ;
1069- const days = activePopper . querySelectorAll ( '.react-datepicker__day' ) ;
1070-
1071- if ( datepicker ) {
1072- datepicker . style . backgroundColor = '#0f172a' ;
1073- datepicker . style . borderColor = '#334155' ;
1074- }
1075-
1076- if ( monthContainer ) {
1077- monthContainer . style . backgroundColor = '#0f172a' ;
1078- }
1079-
1080- if ( header ) {
1081- header . style . backgroundColor = '#1e293b' ;
1082- header . style . borderBottomColor = '#334155' ;
1083- }
1084-
1085- if ( currentMonth ) {
1086- currentMonth . style . color = '#f8fafc' ;
1087- }
1088-
1089- dayNames . forEach ( dayName => {
1090- dayName . style . color = '#e2e8f0' ;
1091- dayName . style . backgroundColor = 'transparent' ;
1092- } ) ;
1093-
1094- days . forEach ( day => {
1095- if ( ! day . classList . contains ( 'react-datepicker__day--selected' ) ) {
1096- day . style . color = '#f8fafc' ;
1097- day . style . backgroundColor = 'transparent' ;
1098- }
1099- } ) ;
1100- } ) ;
1101- } , [ ] ) ;
1102-
1103- const renderCalendarContainer = useCallback (
1104- ( { className, children } ) => (
1105- < CalendarContainer className = { className } > { children } </ CalendarContainer >
1106- ) ,
1107- [ ] ,
1108- ) ;
1109-
11101010 const renderCalendarHeader = useCallback (
11111011 ( { date, decreaseMonth, increaseMonth, prevMonthButtonDisabled, nextMonthButtonDisabled } ) => (
11121012 < div className = { styles . mfkCalendarHeader } >
@@ -1193,8 +1093,6 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
11931093 dateFormat = { isMobile ? 'MM/dd/yyyy' : 'MM/dd/yy' }
11941094 maxDate = { endDate || today }
11951095 minDate = { new Date ( '2023-01-01' ) }
1196- calendarContainer = { renderCalendarContainer }
1197- onCalendarOpen = { applyDarkCalendarTheme }
11981096 renderCustomHeader = { darkMode ? renderCalendarHeader : undefined }
11991097 />
12001098 </ div >
@@ -1213,14 +1111,17 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
12131111 dateFormat = { isMobile ? 'MM/dd/yyyy' : 'MM/dd/yy' }
12141112 minDate = { startDate || new Date ( '2023-01-01' ) }
12151113 maxDate = { today }
1216- calendarContainer = { renderCalendarContainer }
1217- onCalendarOpen = { applyDarkCalendarTheme }
12181114 renderCustomHeader = { darkMode ? renderCalendarHeader : undefined }
12191115 />
12201116 </ div >
12211117 { ( startDate || endDate ) && (
1222- < button className = { styles . clearButton } onClick = { handleClearDates } title = "Clear" >
1223- ✕
1118+ < button
1119+ className = { styles . clearButton }
1120+ onClick = { handleClearDates }
1121+ title = "Clear dates"
1122+ aria-label = "Clear dates"
1123+ >
1124+ < FaTrash size = { 11 } />
12241125 </ button >
12251126 ) }
12261127 </ div >
@@ -1229,22 +1130,18 @@ function MostFrequentKeywords({ darkMode: propDarkMode }) {
12291130 { isLoading && < div className = { styles . mfkLoading } > Loading...</ div > }
12301131 { ! isLoading && error && < div className = { styles . mfkError } > { error } </ div > }
12311132 { ! isLoading && ! error && tags . length === 0 && (
1232- < div className = { styles . mfkEmpty } > { selectedOption ? 'No data' : 'Select source' } </ div >
1133+ < div className = { styles . mfkEmpty } >
1134+ { selectedOption
1135+ ? 'No keywords available for selected filters'
1136+ : 'Select a data source to view keywords' }
1137+ </ div >
12331138 ) }
1234- { ! isLoading && ! error && tags . length > 0 && (
1139+ { ! isLoading && ! error && tags . length > 0 && dimensions . width > 0 && (
12351140 < svg ref = { svgRef } style = { { width : '100%' , height : '100%' } } />
12361141 ) }
12371142 </ div >
12381143 </ div >
12391144 ) ;
12401145}
12411146
1242- MostFrequentKeywords . propTypes = {
1243- darkMode : PropTypes . bool ,
1244- } ;
1245-
1246- MostFrequentKeywords . defaultProps = {
1247- darkMode : false ,
1248- } ;
1249-
12501147export default MostFrequentKeywords ;
0 commit comments