Skip to content

Commit d0d9999

Browse files
Merge pull request #3994 from OneCommunityGlobal/fix/Jobanalytics-date-warning-darkmode
Namitha - fix(JobAnalytics): prevent invalid date range render + react to dark
2 parents 0eeb45d + 251762d commit d0d9999

4 files changed

Lines changed: 581 additions & 210 deletions

File tree

src/components/ApplicationAnalytics/jobAnalytics.jsx

Lines changed: 157 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,189 @@
11
import { useState, useMemo } from 'react';
2-
import { v4 as uuidv4 } from 'uuid';
32
import { useSelector } from 'react-redux';
43
import getJobAnalyticsData from './api';
54
import styles from './jobAnalytics.module.css';
65

76
function JobAnalytics() {
8-
const [dateFilter, setDateFilter] = useState('all');
7+
const { darkMode } = useSelector(state => state.theme);
8+
9+
// Date range (new)
10+
const [startDate, setStartDate] = useState('');
11+
const [endDate, setEndDate] = useState('');
12+
13+
// Role filter
914
const [selectedRole, setSelectedRole] = useState('all');
10-
const [hoveredBar, setHoveredBar] = useState(null);
11-
const darkMode = useSelector(state => state.theme.darkMode);
1215
const rawData = getJobAnalyticsData();
13-
const processedData = useMemo(() => {
16+
17+
// Roles list for the dropdown (includes "all")
18+
const roles = useMemo(() => {
19+
const r = Array.from(new Set(rawData.map(r => r.role))).sort((a, b) => a.localeCompare(b));
20+
return ['all', ...r];
21+
}, [rawData]);
22+
23+
const invalidRange = useMemo(() => {
24+
if (startDate && endDate) return new Date(startDate) > new Date(endDate);
25+
return false;
26+
}, [startDate, endDate]);
27+
28+
const { chartData, maxApplications } = useMemo(() => {
1429
let filtered = [...rawData];
15-
if (dateFilter !== 'all') {
16-
const now = new Date();
17-
filtered = filtered.filter(item => {
18-
const itemDate = new Date(item.timestamp);
19-
const daysAgo = Math.floor((now - itemDate) / (1000 * 60 * 60 * 24));
20-
21-
switch (dateFilter) {
22-
case 'weekly':
23-
if (daysAgo <= 7) return true;
24-
return false;
25-
case 'monthly':
26-
return daysAgo <= 30;
27-
case 'yearly':
28-
return daysAgo <= 365;
29-
default:
30-
return true;
31-
}
30+
31+
// Date range filter (range-first)
32+
if (startDate || endDate) {
33+
const start = startDate ? new Date(`${startDate}T00:00:00`) : new Date('1970-01-01T00:00:00');
34+
const end = endDate ? new Date(`${endDate}T23:59:59`) : new Date();
35+
filtered = filtered.filter(row => {
36+
const d = new Date(row.timestamp);
37+
return d >= start && d <= end;
3238
});
3339
}
40+
41+
// Role filter
3442
if (selectedRole !== 'all') {
35-
filtered = filtered.filter(item => item.role === selectedRole);
43+
filtered = filtered.filter(row => row.role === selectedRole);
44+
}
45+
46+
// Group counts per role
47+
const counts = new Map();
48+
for (const row of filtered) {
49+
counts.set(row.role, (counts.get(row.role) || 0) + 1);
3650
}
37-
const roleGroups = {};
38-
filtered.forEach(item => {
39-
if (!roleGroups[item.role]) {
40-
roleGroups[item.role] = 0;
41-
}
42-
roleGroups[item.role] += 1;
43-
});
44-
const chartData = Object.entries(roleGroups)
45-
.map(([role, applicationCount]) => ({
46-
role,
47-
applications: applicationCount,
48-
hits: Math.floor(applicationCount * (Math.random() * 10 + 5)),
49-
}))
51+
52+
// Build rows and sort least -> most popular
53+
const rows = Array.from(counts.entries())
54+
.map(([role, applications]) => ({ role, applications }))
5055
.sort((a, b) => a.applications - b.applications);
51-
return chartData;
52-
}, [rawData, dateFilter, selectedRole]);
53-
const roles = useMemo(() => {
54-
const uniqueRoles = [...new Set(rawData.map(item => item.role))];
55-
return ['all', ...uniqueRoles];
56-
}, [rawData]);
5756

58-
const maxApplications = Math.max(...processedData.map(item => item.applications), 10);
57+
const max = rows.length ? Math.max(...rows.map(r => r.applications)) : 0;
58+
return { chartData: rows, maxApplications: max };
59+
}, [rawData, startDate, endDate, selectedRole]);
5960

60-
const xAxisTicks = useMemo(() => {
61-
const ticks = [0];
62-
let value = 5;
63-
while (value < maxApplications) {
64-
ticks.push(value);
65-
value += 5;
66-
}
67-
if (maxApplications > 0 && ticks[ticks.length - 1] !== maxApplications) {
68-
ticks.push(maxApplications);
61+
const showingCount = chartData.length;
62+
const least = showingCount ? chartData[0] : null;
63+
const most = showingCount ? chartData[chartData.length - 1] : null;
64+
65+
const ticks = useMemo(() => {
66+
const m = maxApplications || 0;
67+
if (m === 0) return [0];
68+
69+
// Choose a base step aiming for ~4 intervals, but round to friendly numbers
70+
let base = Math.ceil(m / 4);
71+
// If base is at least 5, snap it to nearest multiple of 5 for nicer ticks
72+
if (base >= 5) {
73+
base = Math.max(5, Math.round(base / 5) * 5);
6974
}
70-
return ticks;
75+
76+
// Build ticks from 0 up to the next multiple of base that covers m
77+
const maxTick = Math.ceil(m / base) * base;
78+
const ticksOut = [];
79+
for (let v = 0; v <= maxTick; v += base) ticksOut.push(v);
80+
return ticksOut;
7181
}, [maxApplications]);
7282

7383
return (
74-
<div className={darkMode ? styles.jobAnalyticsContainerDarkMode : ''}>
75-
<div className={styles.jobAnalyticsContainer}>
76-
<div className={styles.chartContainer}>
77-
<h2 className={styles.chartTitle}>Least Popular Roles</h2>
78-
<div
79-
className={styles.chartArea}
80-
style={
81-
processedData.length > 0
82-
? { '--x-grid-divisions': String(Math.max(1, xAxisTicks.length - 1)) }
83-
: undefined
84-
}
85-
>
86-
{processedData.length > 0 ? (
87-
<>
88-
<div className={styles.gridLines} />
89-
<div className={styles.yAxis}>
90-
{processedData.map(item => (
91-
<div key={uuidv4()} className={styles.yAxisLabel}>
92-
{item.role}
93-
</div>
94-
))}
95-
</div>
96-
<div className={styles.xAxis}>
97-
{xAxisTicks.map(tick => (
98-
<div
99-
key={tick}
100-
className={styles.xAxisTick}
101-
style={{
102-
left: `${(tick / maxApplications) * 100}%`,
103-
transform: 'translateX(-50%)',
104-
}}
105-
>
106-
{tick}
107-
</div>
108-
))}
109-
</div>
110-
<div className={styles.barsContainer}>
111-
{processedData.map((item, index) => (
112-
<div
113-
key={uuidv4()}
114-
className={styles.barRow}
115-
onMouseEnter={() => setHoveredBar(index)}
116-
onMouseLeave={() => setHoveredBar(null)}
117-
>
118-
<div
119-
className={styles.bar}
120-
style={{
121-
width: `${(item.applications / maxApplications) * 100}%`,
122-
}}
123-
>
124-
<div className={styles.dataLabel}>{item.applications}</div>
125-
</div>
126-
{hoveredBar === index && (
127-
<div className={styles.tooltip}>
128-
<div className={styles.tooltipTitle}>
129-
<strong>{item.role}</strong>
84+
<div className={`${styles.ja} ${darkMode ? styles.dark : ''}`} style={{ minHeight: '105vh' }}>
85+
<div className={styles.jaMain}>
86+
{/* Left: Chart card */}
87+
<section className={styles.jaCard}>
88+
<h2 className={styles.jaTitle}>Least popular roles</h2>
89+
90+
{invalidRange && (
91+
<div className={styles.jaWarning} role="alert">
92+
Start date cannot be after end date.
93+
</div>
94+
)}
95+
96+
{showingCount ? (
97+
<>
98+
<div className={styles.jaChart}>
99+
<div className={styles.jaGrid} aria-hidden="true" />
100+
<div className={styles.jaBars}>
101+
{chartData.map(row => {
102+
const pct =
103+
maxApplications > 0
104+
? Math.max(2, (row.applications / maxApplications) * 100)
105+
: 0;
106+
return (
107+
<div className={styles.jaRow} key={row.role}>
108+
<div className={styles.jaLabel} title={row.role}>
109+
{row.role}
110+
</div>
111+
<div className={styles.jaTrack}>
112+
<div className={styles.jaBar} style={{ width: `${pct}%` }}>
113+
<span className={styles.jaValue}>{row.applications}</span>
130114
</div>
131-
<div>Applications: {item.applications}</div>
132-
<div>Hits: {item.hits}</div>
133115
</div>
134-
)}
135-
</div>
136-
))}
116+
</div>
117+
);
118+
})}
137119
</div>
138-
<div className={styles.xAxisLabel}>Applications</div>
139-
</>
140-
) : (
141-
<div className={styles.noData}>No data available for the selected filters</div>
142-
)}
143-
</div>
144-
{processedData.length > 0 && (
145-
<div className={styles.summaryInfo}>
146-
<div>
147-
<strong>Showing:</strong> {processedData.length} role(s)
148-
</div>
149-
<div>
150-
<strong>Least Popular:</strong> {processedData[0]?.role} (
151-
{processedData[0]?.applications} applications)
152120
</div>
153-
<div>
154-
<strong>Most Popular:</strong> {processedData[processedData.length - 1]?.role} (
155-
{processedData[processedData.length - 1]?.applications} applications)
121+
122+
<div className={styles.jaXaxis}>
123+
{ticks.map(t => (
124+
<span key={t}>{t}</span>
125+
))}
156126
</div>
157-
</div>
127+
<div className={styles.jaXaxisLabel}>Applications</div>
128+
</>
129+
) : (
130+
<div className={styles.jaNoData}>No data for the selected filters.</div>
158131
)}
159-
</div>
160-
<div className={styles.filtersPanel}>
161-
<div className={styles.filterGroup}>
162-
<div className={styles.filterLabel}>Dates</div>
163-
<select
164-
value={dateFilter}
165-
onChange={e => setDateFilter(e.target.value)}
166-
className={styles.filterSelectJobAnalytics}
167-
>
168-
<option value="all">ALL</option>
169-
<option value="weekly">Last 7 Days</option>
170-
<option value="monthly">Last 30 Days</option>
171-
<option value="yearly">Last Year</option>
172-
</select>
132+
133+
<div className={styles.jaFooter}>
134+
<div>
135+
<strong>Showing:</strong> {showingCount} role(s)
136+
</div>
137+
<div>
138+
<strong>Least Popular:</strong>{' '}
139+
{least ? `${least.role} (${least.applications} applications)` : '—'}
140+
</div>
141+
<div>
142+
<strong>Most Popular:</strong>{' '}
143+
{most ? `${most.role} (${most.applications} applications)` : '—'}
144+
</div>
173145
</div>
174-
<div className={styles.filterGroup}>
175-
<div className={styles.filterLabel}>Role</div>
146+
</section>
147+
148+
{/* Right: Filters */}
149+
<aside className={styles.jaFilters}>
150+
<div className={styles.jaFilter}>
151+
<div className={styles.jaFilterLabel}>Dates</div>
152+
<div className={styles.jaDateRange}>
153+
<input
154+
type="date"
155+
value={startDate}
156+
onChange={e => setStartDate(e.target.value)}
157+
aria-label="Start date"
158+
/>
159+
<span className={styles.jaDateDash}></span>
160+
<input
161+
type="date"
162+
value={endDate}
163+
onChange={e => setEndDate(e.target.value)}
164+
aria-label="End date"
165+
/>
166+
{(startDate || endDate) && (
167+
<button
168+
type="button"
169+
className={styles.jaClear}
170+
onClick={() => {
171+
setStartDate('');
172+
setEndDate('');
173+
}}
174+
>
175+
Clear
176+
</button>
177+
)}
178+
</div>
179+
</div>
180+
181+
<div className={styles.jaFilter}>
182+
<div className={styles.jaFilterLabel}>Role</div>
176183
<select
177184
value={selectedRole}
178185
onChange={e => setSelectedRole(e.target.value)}
179-
className={styles.filterSelectJobAnalytics}
186+
aria-label="Filter by role"
180187
>
181188
{roles.map(role => (
182189
<option key={role} value={role}>
@@ -185,7 +192,7 @@ function JobAnalytics() {
185192
))}
186193
</select>
187194
</div>
188-
</div>
195+
</aside>
189196
</div>
190197
</div>
191198
);

0 commit comments

Comments
 (0)