Skip to content

Commit 9d56b68

Browse files
Merge pull request #5307 from OneCommunityGlobal/vinay_k_render_keywords
Vinay K Most Frequent Keywords
2 parents 1ae9064 + f0ab5d2 commit 9d56b68

3 files changed

Lines changed: 103 additions & 192 deletions

File tree

src/components/BMDashboard/WeeklyProjectSummary/MostFrequentKeywords/MostFrequentKeywords.jsx

Lines changed: 79 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useEffect, useRef, useState, useCallback } from 'react';
22
import { useSelector } from 'react-redux';
33
import axios from 'axios';
4-
import DatePicker, { CalendarContainer } from 'react-datepicker';
4+
import DatePicker from 'react-datepicker';
55
import 'react-datepicker/dist/react-datepicker.css';
66
import * as d3 from 'd3';
7+
import { FaTrash } from 'react-icons/fa';
78
import styles from './MostFrequentKeywords.module.css';
89
import Select, { components as selectComponents } from 'react-select';
9-
import PropTypes from 'prop-types';
10-
1110
const 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-
12501147
export default MostFrequentKeywords;

0 commit comments

Comments
 (0)